package org.jugile.restapi.domain;

import org.jugile.restapi.common.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.log4j.Logger;

import org.jugile.util.*;


public abstract class DomainBase extends Jugile implements java.io.Serializable {
	static Logger log = Logger.getLogger(DomainBase.class);
	
	private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	private final Lock readLock = readWriteLock.readLock(); 
	private final Lock writeLock = readWriteLock.writeLock();

	private static Domain domain;
	private static boolean initializing = false;
	public static Domain getDomain() { return getDomain(null);}
	public synchronized static Domain getDomain(String tag) {
		if (domain != null) {
			// check update
			if (domain._hasDb()) {
				if (domain._needsUpdate()) {
					Timer t = new Timer();
					int count = domain._readAllUpdates();
					log.info("updated from db: " + count + " " + t.stop() + " ms");
					//log("  reindexing");
					//domain.reindex(); // TODO: reindex only updated
					// reindexing done from readupdate object
				}
			}
			return domain;
		}
		initializing = true;
		domain = new Domain();
		if (empty(tag)) {
			// default load
			if (domain._hasDb()) {
				Timer t = new Timer();
				int count = domain._loadAll();
				log.info("loaded from db: " + count + " " + t.stop() + " ms");
			}
		}
		World.init(domain, tag);
		initializing = false;
		return domain;
	}
	
	public static void load(String fname) { domain = (Domain)loadObject(fname); }
	public void save(String fname) { saveObject(this,fname); }
	public static void reset() { Global.reset(); domain = null; }

	private Global global; // stored here for saving
	public Global global() { 
		if (global == null) global = Global.get(); 
		return global;
	}

	public Object get(String key) { return uow.get().get(key); }
	public void put(String key, Object value) { uow.get().put(key,value); }

	
	public void init(String tag) {
		World.init((Domain)this, tag);
	}

	// profiler
	public void resetStats() { 	Profiler.reset();}
	public String dumpStats() { return Profiler.dump(); }

	private long nextid = 100000;
	public long getNextId(long id) {
		if (!_hasDb()) {
			if (id == 0) return nextid++;
			if (nextid < id) nextid = id+1; // adjust nextid from imported
			return id;
		}
		if (id == 0) return getNextId();
		return id; // no adustments needed in db hilo
	}

	public long getNextId() {
		if (!_hasDb()) {
			log.fatal("no db for getNextId()");
			fail("no db for getNextId()");
		}
		if (idHi == 0 || idLo >= idHi) {
			idHi = getNextIdHiFromDb("global");
			idLo = idHi - idIncrement;
		}
		return idLo++;
	}
	
	private long idHi = 0;
	private long idLo = 0;
	private final static long idIncrement = 100;
	public static synchronized long getNextIdHiFromDb(String obj) {		
		long nid = 0;
		DBPool pool = DBPool.getPool();
		DBConnection c = pool.getConnection();
		try {
			c.writeTx();
			c.prepare("select nextid from idpool where obj=?");
			c.param(obj);
			List<List> res = c.select();
			nid = (Integer)res.get(0).get(0);
			c.prepare("update idpool set nextid=? where obj=?");
			c.param(nid+idIncrement);
			c.param(obj);
			c.execute();
			c.commit();
			c.free();
		} catch (Exception e) { fail(e); } 
		return nid;
	}
	
	
	// ---- doh ----
	public IDO get(DOH doh) {
		if (doh == null) return null;		
		if ("Api".equals(doh.getDomainClassName())) return getApi(doh.getId());
		if ("Operation".equals(doh.getDomainClassName())) return getOperation(doh.getId());
		if ("Part".equals(doh.getDomainClassName())) return getPart(doh.getId());
		if ("PathItem".equals(doh.getDomainClassName())) return getPathItem(doh.getId());
		if ("Type".equals(doh.getDomainClassName())) return getType(doh.getId());
		if ("Var".equals(doh.getDomainClassName())) return getVar(doh.getId());
		// fail("unknown domain object: " + doh.getDomainClassName());
		return null; // needed for get(new DOH(null))
	}
	
	// ---- services -----

	// ---- object maps -----
	
	private Bos<Api> apis = new Bos<Api>();
	protected Api createApi() { 
		Api o = _createEmptyApi(0);
		o.setVirgin(true);
		return o;
	}
	public Api _createEmptyApi(String id) { return _createEmptyApi(parseLongSafe(id)); }
	public Api _createEmptyApi(long id) {
		id = getNextId(id);
		Api o = new Api();
		o.id(id);
		apis.add(o);
		return o;
	}
	public Api getApi(String id) { return getApi(parseLongSafe(id)); }
	public Api getApi(long id) { return (Api)apis.get(id); }
	public ApiCollection getApis() {
		return new ApiCollection(apis);
	}
	public void deleteApi(String id) { deleteApi(parseLongSafe(id));	}
	public void deleteApi(long id) { apis.remove(id);	}

	protected ApiCollection findApisByName(String p) {
		ApiCollection res = getApis().byName(p);
		return res;
	}
	protected ApiCollection findApisByFname(String p) {
		ApiCollection res = getApis().byFname(p);
		return res;
	}
	
	private Bos<Operation> operations = new Bos<Operation>();
	protected Operation createOperation() { 
		Operation o = _createEmptyOperation(0);
		o.setVirgin(true);
		return o;
	}
	public Operation _createEmptyOperation(String id) { return _createEmptyOperation(parseLongSafe(id)); }
	public Operation _createEmptyOperation(long id) {
		id = getNextId(id);
		Operation o = new Operation();
		o.id(id);
		operations.add(o);
		return o;
	}
	public Operation getOperation(String id) { return getOperation(parseLongSafe(id)); }
	public Operation getOperation(long id) { return (Operation)operations.get(id); }
	public OperationCollection getOperations() {
		return new OperationCollection(operations);
	}
	public void deleteOperation(String id) { deleteOperation(parseLongSafe(id));	}
	public void deleteOperation(long id) { operations.remove(id);	}

	protected OperationCollection findOperationsByMethod(String p) {
		OperationCollection res = getOperations().byMethod(p);
		return res;
	}
	protected OperationCollection findOperationsByTmpl(String p) {
		OperationCollection res = getOperations().byTmpl(p);
		return res;
	}
	protected OperationCollection findOperationsByModel(String p) {
		OperationCollection res = getOperations().byModel(p);
		return res;
	}
	protected OperationCollection findOperationsByMimetype(String p) {
		OperationCollection res = getOperations().byMimetype(p);
		return res;
	}
	protected OperationCollection findOperationsByBorder(String p) {
		OperationCollection res = getOperations().byBorder(p);
		return res;
	}
	
	private Bos<Part> parts = new Bos<Part>();
	protected Part createPart() { 
		Part o = _createEmptyPart(0);
		o.setVirgin(true);
		return o;
	}
	public Part _createEmptyPart(String id) { return _createEmptyPart(parseLongSafe(id)); }
	public Part _createEmptyPart(long id) {
		id = getNextId(id);
		Part o = new Part();
		o.id(id);
		parts.add(o);
		return o;
	}
	public Part getPart(String id) { return getPart(parseLongSafe(id)); }
	public Part getPart(long id) { return (Part)parts.get(id); }
	public PartCollection getParts() {
		return new PartCollection(parts);
	}
	public void deletePart(String id) { deletePart(parseLongSafe(id));	}
	public void deletePart(long id) { parts.remove(id);	}

	protected PartCollection findPartsByName(String p) {
		PartCollection res = getParts().byName(p);
		return res;
	}
	protected PartCollection findPartsByDesc(String p) {
		PartCollection res = getParts().byDesc(p);
		return res;
	}
	protected PartCollection findPartsByRegexp(String p) {
		PartCollection res = getParts().byRegexp(p);
		return res;
	}
	protected PartCollection findPartsByExample(String p) {
		PartCollection res = getParts().byExample(p);
		return res;
	}
	protected PartCollection findPartsByIdx(int p) {
		PartCollection res = getParts().byIdx(p);
		return res;
	}
	protected PartCollection findPartsByVar(String p) {
		PartCollection res = getParts().byVar(p);
		return res;
	}
	
	private Bos<PathItem> pathItems = new Bos<PathItem>();
	protected PathItem createPathItem() { 
		PathItem o = _createEmptyPathItem(0);
		o.setVirgin(true);
		return o;
	}
	public PathItem _createEmptyPathItem(String id) { return _createEmptyPathItem(parseLongSafe(id)); }
	public PathItem _createEmptyPathItem(long id) {
		id = getNextId(id);
		PathItem o = new PathItem();
		o.id(id);
		pathItems.add(o);
		return o;
	}
	public PathItem getPathItem(String id) { return getPathItem(parseLongSafe(id)); }
	public PathItem getPathItem(long id) { return (PathItem)pathItems.get(id); }
	public PathItemCollection getPathItems() {
		return new PathItemCollection(pathItems);
	}
	public void deletePathItem(String id) { deletePathItem(parseLongSafe(id));	}
	public void deletePathItem(long id) { pathItems.remove(id);	}

	protected PathItemCollection findPathItemsByName(String p) {
		PathItemCollection res = getPathItems().byName(p);
		return res;
	}
	protected PathItemCollection findPathItemsByDesc(String p) {
		PathItemCollection res = getPathItems().byDesc(p);
		return res;
	}
	protected PathItemCollection findPathItemsByStr(String p) {
		PathItemCollection res = getPathItems().byStr(p);
		return res;
	}
	protected PathItemCollection findPathItemsByRegexp(String p) {
		PathItemCollection res = getPathItems().byRegexp(p);
		return res;
	}
	protected PathItemCollection findPathItemsByTmplDir(String p) {
		PathItemCollection res = getPathItems().byTmplDir(p);
		return res;
	}
	protected PathItemCollection findPathItemsByModelDir(String p) {
		PathItemCollection res = getPathItems().byModelDir(p);
		return res;
	}
	protected PathItemCollection findPathItemsByPkg(String p) {
		PathItemCollection res = getPathItems().byPkg(p);
		return res;
	}
	protected PathItemCollection findPathItemsByController(String p) {
		PathItemCollection res = getPathItems().byController(p);
		return res;
	}
	protected PathItemCollection findPathItemsByDcontroller(String p) {
		PathItemCollection res = getPathItems().byDcontroller(p);
		return res;
	}
	protected PathItemCollection findPathItemsByRedirect(String p) {
		PathItemCollection res = getPathItems().byRedirect(p);
		return res;
	}
	protected PathItemCollection findPathItemsByVar(String p) {
		PathItemCollection res = getPathItems().byVar(p);
		return res;
	}
	protected PathItemCollection findPathItemsByBorder(String p) {
		PathItemCollection res = getPathItems().byBorder(p);
		return res;
	}
	protected PathItemCollection findPathItemsByType(PathItemType p) {
		PathItemCollection res = getPathItems().byType(p);
		return res;
	}
	protected PathItemCollection findPathItemsByCondition(String p) {
		PathItemCollection res = getPathItems().byCondition(p);
		return res;
	}
	
	private Bos<Type> types = new Bos<Type>();
	protected Type createType() { 
		Type o = _createEmptyType(0);
		o.setVirgin(true);
		return o;
	}
	public Type _createEmptyType(String id) { return _createEmptyType(parseLongSafe(id)); }
	public Type _createEmptyType(long id) {
		id = getNextId(id);
		Type o = new Type();
		o.id(id);
		types.add(o);
		return o;
	}
	public Type getType(String id) { return getType(parseLongSafe(id)); }
	public Type getType(long id) { return (Type)types.get(id); }
	public TypeCollection getTypes() {
		return new TypeCollection(types);
	}
	public void deleteType(String id) { deleteType(parseLongSafe(id));	}
	public void deleteType(long id) { types.remove(id);	}

	protected TypeCollection findTypesByName(String p) {
		TypeCollection res = getTypes().byName(p);
		return res;
	}
	protected TypeCollection findTypesByDesc(String p) {
		TypeCollection res = getTypes().byDesc(p);
		return res;
	}
	protected TypeCollection findTypesBySuffix(String p) {
		TypeCollection res = getTypes().bySuffix(p);
		return res;
	}
	protected TypeCollection findTypesByMimetype(String p) {
		TypeCollection res = getTypes().byMimetype(p);
		return res;
	}
	
	private Bos<Var> vars = new Bos<Var>();
	protected Var createVar() { 
		Var o = _createEmptyVar(0);
		o.setVirgin(true);
		return o;
	}
	public Var _createEmptyVar(String id) { return _createEmptyVar(parseLongSafe(id)); }
	public Var _createEmptyVar(long id) {
		id = getNextId(id);
		Var o = new Var();
		o.id(id);
		vars.add(o);
		return o;
	}
	public Var getVar(String id) { return getVar(parseLongSafe(id)); }
	public Var getVar(long id) { return (Var)vars.get(id); }
	public VarCollection getVars() {
		return new VarCollection(vars);
	}
	public void deleteVar(String id) { deleteVar(parseLongSafe(id));	}
	public void deleteVar(long id) { vars.remove(id);	}

	protected VarCollection findVarsByName(String p) {
		VarCollection res = getVars().byName(p);
		return res;
	}
	protected VarCollection findVarsByValue(String p) {
		VarCollection res = getVars().byValue(p);
		return res;
	}
	
 
    // --- utils ---
	public Class getCl(String classname) {
		if ("Api".equals(classname)) return Api.class; 
		if ("Operation".equals(classname)) return Operation.class; 
		if ("Part".equals(classname)) return Part.class; 
		if ("PathItem".equals(classname)) return PathItem.class; 
		if ("Type".equals(classname)) return Type.class; 
		if ("Var".equals(classname)) return Var.class; 
 		if ("String".equals(classname)) return String.class;
		if ("int".equals(classname)) return int.class;
		if ("Integer".equals(classname)) return Integer.class;
		if ("Time".equals(classname)) return Time.class;
		if ("Money".equals(classname)) return Money.class;
		if ("Object".equals(classname)) return Object.class;
		fail("unknown type: " + classname);
		return Object.class;
    }

	// --- fail and log ---



	// --- load and save ---

	public int writeCsv(String fname) throws Exception {
		int count = 0;
		Writer out = zipWriter(fname);
		writeCsv(out, "Api", getApis().list());
		writeCsv(out, "Operation", getOperations().list());
		writeCsv(out, "Part", getParts().list());
		writeCsv(out, "PathItem", getPathItems().list());
		writeCsv(out, "Type", getTypes().list());
		writeCsv(out, "Var", getVars().list());
		// write n-n mappings
		String cn1 = null;
		String cn2 = null;

 
		out.close();
		return count;
	}

	//public abstract int writeCsv(Writer out, String cname, List items) throws Exception;

	public int writeCsv(Writer out, String cname, List items) throws Exception {
		int count = 0;
		String hdr = "#"+cname;
		String hdrs[] = (String[])Proxy.staticCall("org.jugile.restapi.domain."+cname, "flds");
		for (String h : hdrs) hdr += "," + h;
		out.write(hdr + "\n");
		for (IDO o : (List<IDO>)items) {
			Proxy p = new Proxy(o);
			out.write(o.getId());
			for (String h : hdrs) {
				Object v = p.call("get"+up(h));
				String str = toCsv(v);
				//print("hdr: " + h + "=" + str);
				out.write(","+str);
			}
			out.write("\n");
			count++;
		}
		return count;
	}
	

	public int loadCsv(String fname) throws Exception {
		print("loadCsv: " + fname);
		int count = 0;
		CsvTokenizer t = new CsvTokenizer(zipInputStream(fname));
		String cname = null;
		List<String> headers = null;
		boolean mapping = false;
		String cn1 = null;
		String cn2 = null;
		while (t.readLine()) {
			if (t.col(0).startsWith("#")) {
				// headerline
				cname = t.col(0).substring(1);
				headers = new ArrayList<String>();
				for (String hdr : t.getCols()) headers.add(hdr);
				headers.set(0, "id");
				mapping = false;
				continue;
			}
			if (t.col(0).startsWith("@")) {
				// n-n mapping
				cn1 = t.col(0).substring(1);
				cn2 = t.col(1);
				mapping = true;				
				continue;
			}

			if (mapping) {
				IDO o1 = getOrCreate(cn1, t.col(0));
				Proxy bo1 = new Proxy(o1);
				if (o1 == null) fail("invalid object: "+ cn1 + " "+ t.col(0));
				IDO o2 = getOrCreate(cn2, t.col(1));
				Proxy bo2 = new Proxy(o2);
				if (o2 == null) fail("invalid object: "+ cn2 + " "+ t.col(1));
				bo1.call("add"+cn2,o2);
			} else {
				// dataline
				if (empty(cname)) continue;
				// get or create object by id
				
				IDO o = getOrCreate(cname, t.col(0));
				if (o == null) fail("invalid object: "+ cname + " "+ t.col(0));
				Proxy bo = new Proxy(o);
				// set fields
				for (int i = 1; i < t.cols(); i++) {
					String setter = "parse" + up(headers.get(i));
					String v = t.col(i);
					bo.call(setter, v);
				}
				Entity eo = (Entity)o;
				eo.onLoad((Domain)this);
			}
			count++;
		}
		t.close();
		return count;
	}

	protected IDO getOrCreate(String cname, String id) {
		Proxy d = new Proxy(this);
		if (empty(id)) return null;
		// get or create object by id
		IDO o = (IDO)d.call("get"+cname, id);
		if (o == null) {
			o = (IDO)d.call("_createEmpty"+cname,id);
			((Entity)o).setVirgin(true);
		}
		return o;		
	}

	protected String toCsv(Object o) {
		if (o == null) return "-";
		if (o instanceof IDO) return ((IDO)o).getId();
		if (o instanceof Time) return ""+((Time)o).getMs();
		if (o instanceof Duration) return ""+((Duration)o).getMs();
		if (o instanceof String) {
			String str = Node.replaceControls((String)o);
			str = Jugile.escape(str, ',');
			return "\"" + str + "\"";
		}
		if (o instanceof EnumType) return ""+((EnumType)o).getValue();
		// int, float
		return o.toString();
	}
	
	
	// ----- db mappings ------

	private static String hostname;
	public static String getHostName() {
		if (hostname != null) return hostname;
		hostname = resolveHostname();
		return hostname;
	}
	private static synchronized String resolveHostname() {
		return Jugile.getHostName();
	}
	
	private static String hostnum;
	public static String getHostNum() {
		if (hostnum != null) return hostnum;
		hostnum = "S"+nn(getHostName()).replaceAll("[^0-9]+", "");
		return hostnum;
	}
	
	protected void _saveMapping(DBConnection c, String table, Entity o1, Entity o2) throws Exception {
		c.prepare("insert into "+table+" set o1=?,o2=?,host=? "
				+ " on duplicate key update host=?");
		c.param(o1.id());
		c.param(o2.id());
		c.param(getHostName());
		c.param(getHostName());
		c.execute();
	}
	
	public void _save(DBConnection c, Entity o) throws Exception {
		o._writeFlds(c);
	}
	
	public void _delete(DBConnection c, Entity o) throws Exception {
		c.prepare("delete from " + o._getTableName() + " where id_f=? AND vers=?");
		c.param(o.id());
		c.param(o.getVersion());
		c.execute();
		c.prepare("insert into deleted_objs_t set doh=?,host=?");
		c.param(o.getDoh().toString());
		c.param(getHostName());
		c.execute();
	}

	private boolean isInitial(Time ts) {
		Time ts2 = new Time("1.1.1971");
		return ts2.gt(ts);
	}
	
	public synchronized int _readAllUpdates(Time ts, String host) throws Exception {
		DBPool pool = DBPool.getPool();
		DBConnection c = pool.getConnection();
		int count = 0;
		int cnt = 0;
		Buffer buf = new Buffer();
		try {
			// objects
			cnt = Api._readAllUpdates((Domain)this,c,ts,host);
			buf.ln("dbread Api : " + cnt); count += cnt; 
			cnt = Operation._readAllUpdates((Domain)this,c,ts,host);
			buf.ln("dbread Operation : " + cnt); count += cnt; 
			cnt = Part._readAllUpdates((Domain)this,c,ts,host);
			buf.ln("dbread Part : " + cnt); count += cnt; 
			cnt = PathItem._readAllUpdates((Domain)this,c,ts,host);
			buf.ln("dbread PathItem : " + cnt); count += cnt; 
			cnt = Type._readAllUpdates((Domain)this,c,ts,host);
			buf.ln("dbread Type : " + cnt); count += cnt; 
			cnt = Var._readAllUpdates((Domain)this,c,ts,host);
			buf.ln("dbread Var : " + cnt); count += cnt; 


			if (!isInitial(ts)) {
				// deletions
				if (host == null) {
					c.prepare("select ts,host,doh from deleted_objs_t where ts >= ?");			
					c.param(ts);
				} else {
					c.prepare("select ts,host,doh from deleted_objs_t where ts >= ? AND host != ?");			
					c.param(ts);
					c.param(host);
				}
				cnt = 0;
				for (List row : c.select()) {
					String doh = (String)row.get(2);
					// just remove from domain - do not call onDelete()
					IDO o = get(new DOH(doh));
					if (o != null) o.delete(false);
					cnt++;
				}
				buf.ln("delete events: " + cnt); count += cnt; 
				
				// delete n-n mappings
				if (host == null) {
					c.prepare("select ts,host,doh1,doh2,name from deleted_maps_t where ts >= ?");			
					c.param(ts);
				} else {
					c.prepare("select ts,host,doh1,doh2,name from deleted_maps_t where ts >= ? AND host != ?");			
					c.param(ts);
					c.param(host);
				}
				cnt = 0;
				for (List row : c.select()) {
					String doh1 = (String)row.get(2);
					String doh2 = (String)row.get(3);
					String name = (String)row.get(4);
					IDO o1 = get(new DOH(doh1));
					if (o1 == null) continue;
					IDO o2 = get(new DOH(doh2));
					if (o2 == null) continue;
					
					// all n-n cases here
					cnt++;
				}
				buf.ln("delete n-n events: " + cnt); count += cnt; 
				
			}

		} catch (Exception e) {
			log.error(buf.toString());
			throw e;
		} finally {
			c.free();
		}
		if (count > 0) log.info(buf.toString());
		return count;
	}

	public int saveAllToDB() { _saveAll(); return 0; }
	public synchronized void _saveAll() {
		DBPool pool = DBPool.getPool();
		DBConnection c = pool.getConnection();

		try {
			// objects
			for (Api o : getApis()) _save(c,o);
			for (Operation o : getOperations()) _save(c,o);
			for (Part o : getParts()) _save(c,o);
			for (PathItem o : getPathItems()) _save(c,o);
			for (Type o : getTypes()) _save(c,o);
			for (Var o : getVars()) _save(c,o);


			c.commit();
		} catch (Exception e) {
			log.debug("_saveAll failed: " + e);
			try {c.rollback();} catch(Exception e2) {}
			fail(e);
		} finally {
			try {
				c.free();
			} catch(Exception e2) {
				log.fatal("failed to free connection",e2);
				fail(e2);
			}
		}
		
	}
	
	// ------- db update -------
	private Time _lastUpdate = new Time(0);
	public int _readAllUpdates() { return _readAllUpdates(getHostName()); }
	public int _loadAll() { return _readAllUpdates(null); }
	public synchronized int _readAllUpdates(String host) {
		if (!_hasDb()) return 0;
		Time ts = _lastUpdate;
		Time start = getServerTime();
		int count = 0;
		if (ts != null) log.info("readAllUpdates ts: " + ts.toJson());
		if (start != null) log.info("readAllUpdates start: " + start.toJson());
		Timer timer = new Timer();
		try {
			_lastUpdate = start;
			if (!ts.eq(new Time(0))) {
				ts = ts.minus(new Duration(60000L)); // 60 sec window
			}
			count = _readAllUpdates(ts,host);
		} catch (Exception e) {
			_lastUpdate = new Time(0);
			log.error("error in readAllUpdates", e);
		} finally {
			_updateSemaphore = false;
			log.info("readAllUpdates took ms: " + timer.stop());
		}
		return count;
	}
	
	protected Time getServerTime() {
		return new Time(new Date());
	}
	
	private static boolean _updateSemaphore = false;
	public synchronized boolean _needsUpdate() {
		if (initializing) return false;
		if (_updateSemaphore) return false;
		Duration dur = getServerTime().minus(_lastUpdate);
		if (dur.getSeconds() > 40) {
			_updateSemaphore = true;
			return true;
		}
		return false;
	}

	
	private Boolean _hasDb = null;
	public boolean _hasDb() {
		if (_hasDb != null) return _hasDb;
		String secs = Props.get("org.jugile.restapi.db.update");
		if (!empty(secs)) _hasDb = true;
		else _hasDb = false;
		return _hasDb;
	}
	
	
	// ----- uow ------
	
	private static ThreadLocal<UoW> uow = 
		new ThreadLocal<UoW>() {
		protected synchronized UoW initialValue() {
			return new UoW();
		}
	};
	
	protected void _modified(Entity o) {
		if (!_hasDb()) return;
		uow.get().setModified(o);
	}
	protected void _deleted(Entity o) {
		if (!_hasDb()) return;
		uow.get().setDeleted(o);
	}
	protected void _map(String name, Entity o1, Entity o2, String cn) {
		if (!_hasDb()) return;
		uow.get().addMapping(name, o1, o2, cn);
	}
	protected void _unmap(String name, Entity o1, Entity o2, String cn) {
		if (!_hasDb()) return;
		uow.get().removeMapping(name, o1, o2, cn);
	}
	public final int getCommitSize() {
		if (!_hasDb()) return 0;
		return uow.get().getCommitSize();
	}

	public void startTx() {
		if (!_hasDb()) return;
		uow.remove();
	}
	public void rollback() {
		if (!_hasDb()) return;
		if (!isAdminNode()) return;
		if (uow.get().getCommitSize() == 0) return;
		for (Entity o : uow.get().getModified()) {
			if (o.isVirgin()) o.delete(); // delete uncommited objects from domain
		}
		// TODO: implement mem rollback -> recover copies
		uow.remove();
	}
	
	public boolean isAdminNode() {
		return Props.isAdminNode();
	}

	
	public synchronized void commit() {
		if (!_hasDb()) return;
		if (!isAdminNode()) return;
		if (uow.get().getCommitSize() == 0) return;
		DBPool pool = DBPool.getPool();
		DBConnection c = pool.getConnection();
		try {
			this._updateSemaphore = true;
			int delcount = 0;
			for (Entity o : uow.get().getDeletedSorted()) {
				_delete(c,o);
				delcount++;
				uow.get().removeModified(o);
			}
			log.debug("removed deleted: " + delcount);
			int count = 0;
			for (Entity o : uow.get().getModifiedSorted()) {
				o._writeFlds(c);
				count++;
			}
			log.debug("wrote modified: " + count);
			delcount = 0;
			for (UoW.Mapping m : uow.get().getMappings()) {
				c.prepare("insert into "+m.getTableName()+" set o1=?,o2=?,host=? "
						+ " on duplicate key update host=?");
				c.param(m.getO1().id());
				c.param(m.getO2().id());
				c.param(getHostName());
				c.param(getHostName());
				c.execute();
				delcount++;
			}
			log.debug("mapped: " + delcount);
			delcount = 0;
			for (UoW.Mapping m : uow.get().getDelMappings()) {
				c.prepare("delete from "+m.getTableName()+" where o1=? AND o2=?");
				c.param(m.getO1().id());
				c.param(m.getO2().id());
				c.execute();
				c.prepare("insert into deleted_maps_t set doh1=?,doh2=?,name=?,host=?");
				c.param(m.getO1().getDoh().toString());
				c.param(m.getO2().getDoh().toString());
				c.param(m.getName());
				c.param(getHostName());
				c.execute();
				delcount++;
			}
			log.debug("unmapped: " + delcount);
			c.commit();

			// mark not virgins - if commit fully ok 
			for (Entity o : uow.get().getModifiedSorted()) {
				if (!o.isVirgin()) o.incrVersion(); // update modified versions
				o.setVirgin(false);
			}

		} catch (Exception e) {
			log.error("commit failed - doing domain rollback", e);
			rollback(); // domain rollback
			try { c.rollback(); } catch (Exception e2) { fail("db rollback failed: " +e2);}
			fail("commit failed: " + e);
		} finally {
			log.info(uow.get().getInfo());
			uow.remove(); // remove commited unit of work
			try { c.free(); } catch (Exception e2) {}
			this._updateSemaphore = false;
		}
		// do update allways automatically after write commit
		_readAllUpdates();
	}

	public void finalizeTransaction(){}
	public void close() {}

	public void saveToCsvZip(String fname) throws Exception {
		writeCsv(fname);
	}
	public void saveToCsv(String fname) throws Exception {
		writeCsv(fname);
	}
	public void loadFromCsvZip(String fname) throws Exception {
		loadCsv(fname);
	}
	public void loadFromCsv(String fname) throws Exception {
		loadCsv(fname);
	}

	protected int readDeltasFromQueue(int max) {
		return 0;
	}
	
	public void setReadTx() {
		// TODO Auto-generated method stub
	}

	public void setWriteTx() {
		// TODO Auto-generated method stub
	}

}